﻿using System;
using System.Reflection;

namespace PGMapper
{
    /// <summary>
    /// Represents a mapping between a Postgres column and a property or field (possibly marked with 
    /// a [Column] attribute) on a type (possibly marked with a [Table] attribute). The Column 
    /// attribute is used to strongly bind a column name to a property or field. If it is null, a 
    /// "best guess" will be used. The Table attribute is used to generate constraint names when
    /// migrating schema.
    /// 
    /// Persists for caching purposes.
    /// </summary>
    public class MappedProperty
    {
        public TableAttribute Table { get; private set; }
        public ColumnAttribute Column { get; private set; }
        public PropertyInfo Property { get; private set; }
        public FieldInfo Field { get; private set; }

        /// <summary>
        /// Creates a mapping between a Postgres column and a property on a type. The column and table args may be null.
        /// </summary>
        public MappedProperty(PropertyInfo property, ColumnAttribute column, TableAttribute table)
        {
            this.Property = property;
            this.Column = column;
            this.Table = table;
        }

        /// <summary>
        /// Creates a mapping between a Postgres column and a field on a type. The column arg may be null.
        /// </summary>
        public MappedProperty(FieldInfo field, ColumnAttribute column)
        {
            this.Field = field;
            this.Column = column;
        }

        public void Set(object obj, object value)
        {
            Type type = Property != null ? Property.PropertyType : Field.FieldType;
            
            // set enums from string if you've applied [EnumValue]
            if (type.IsEnum && value is string)
                value = EnumValueAttribute.ConvertToEnumIfApplied(type, (string)value);
            else if (type.IsReference() && value != null)
            {
                if (value is int)
                    value = type.CreateReference((int)value);
                else if (value != null)
                    throw new InvalidCastException(string.Format("Could not set the property {0}.{1} because Reference<> types can only accept integers.",
                        obj.GetType().Name, Property.Name));
            }
            // take care of a really common case - postgres returns Int64 types for aggregate
            // query operators, when 99% of the time we just expect an int.
            else if (type == typeof(int) && value is long)
                value = Convert.ToInt32(value);
            // convert strings like "YES" or "NO" to a bool
            else if (type == typeof(bool) && value is string)
                value = ((string)value).ToLower().Contains("y");

            try {
                if (Property != null)
                    Property.SetValue(obj, value, BindingFlags.NonPublic, null, null, null);
                else
                    Field.SetValue(obj, value);
            }
            catch (Exception e)
            {
                throw new InvalidCastException(string.Format("Could not set the {0} {1}.{2}.",
                    Property != null ? "property" : "field", obj.GetType().Name, (Property ?? (MemberInfo)Field).Name), e);
            }
        }

        public object Get(object obj)
        {
            Type type = Property.PropertyType;

            object value = Property.GetValue(obj, null);

            // return enums as either int or string, depending on if you've applied [EnumValue]
            if (type.IsEnum)
            {
                value = EnumValueAttribute.ConvertToStringIfApplied(type, value);

                // did it work? if not, cast to int
                if (!(value is string)) value = Convert.ToInt32(value);
            }
            else if (type.IsReference() && value != null)
                value = ((IReference)value).ID;

            return value;
        }

        public string GetUniqueConstraintName()
        {
            // this is the formula Postgres uses to create constraints from the UNIQUE keyword
            return string.Format("{0}_{1}_key", Table.Name, Column.Name);
        }

        public string GetForeignKeyConstraintName()
        {
            // this is the formula Postgres uses to create constraints from the REFERENCES keyword
            return string.Format("{0}_{1}_fkey", Table.Name, Column.Name);
        }
    }
}
